// evalState:
//     { statements   : statement[],
//       current      : int }
// evalStack := evalState[]
// statement:
//     STEP
//   | TURNLEFT
//   | PUT_BRICK
//   | TAKE_BRICK
//   | PUT_MARK
//   | TAKE_MARK
//   | REPEAT n statement[]
//   | WHILE condition statement[]
//   | CALL name
// condition:
//   IS_WALL
//   IS_NO_WALL
//   IS_MARK
//   IS_NO_MARK
//   IS_NORTH
//   IS_SOUTH
//   IS_EAST
//   IS_WEST
// environment: 
//   string -> statement[]

var maxBricks = 9;
var maxMarks = 9;

function DirToDiff(dir) {
    switch (dir) {
        case 0:
            return { x : 0, y : 1 };
        case 1:
            return { x : 1, y : 0 };
        case 2:
            return { x : 0, y : -1 };
        case 3:
            return { x : -1, y : 0 };
    }
}

function positionInField(field, pos) {
    return pos.x < field[0].length && pos.x >= 0 &&
           pos.y < field.length && pos.y >= 0;
}

function getNextPos(pos) {
    var diff = DirToDiff(pos.dir);
    return { x : pos.x + diff.x, y : pos.y + diff.y, dir : pos.dir };
}

function evalCondition(condition, fieldState) {
    var res = false;

    switch (condition.kind) {
        case "BRICK":
            var newPos = getNextPos(fieldState.karelPos);
            if (!positionInField(fieldState.field, newPos)) {
                res = false;
            }
            else {
                res = fieldState.field[newPos.y][newPos.x].numBricks > 0;
            }
            break;
            
        case "MARK":
            res = fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numMarks > 0;
            break;
            
        case "FREE":
            var nextPos = getNextPos(fieldState.karelPos);
            if (!positionInField(fieldState.field, nextPos)) {
                res = false;
            }
            else {
                var currBricks = fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numBricks;
                var nextBricks = fieldState.field[nextPos.y][nextPos.x].numBricks;
                res = currBricks - nextBricks <= 1 && nextBricks - currBricks <= 1;
            }
            break;
            
        case "NORTH":
            res = fieldState.karelPos.dir === 0;
            break;
             
        default:
            throw "Invalid condition";
    }

    if (condition.negated) {
        res = !res;
    }

    return res;
}

function evalStep(evalStack, environment, fieldState) {
    var frame = evalStack[evalStack.length - 1];

    if (frame.current === frame.statements.length) {
        // We finished statements in the block ->
        // need to figure out what to do next
        if (frame.continuation === "REPEAT") {
            if (frame.count > 0) {
                frame.count--;
                frame.current = 0;
                return "OK";
            }
        }
        else if (frame.continuation === "WHILE") {
            if (evalCondition(frame.condition, fieldState)) {
                frame.current = 0;
                return "OK";
            }
        }

        if (evalStack.length === 1) {
            return "FINISHED";
        }
        evalStack.pop();
        return "OK";
    }

    var stmt = frame.statements[frame.current];
    
    switch (stmt.kind) {
        case "STEP":
            var newPos = getNextPos(fieldState.karelPos);

            // Validate
            var valid = positionInField(fieldState.field, newPos);

            if (valid) {
                var currBricks = fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numBricks;
                var nextBricks = fieldState.field[newPos.y][newPos.x].numBricks;
                valid = (currBricks <= nextBricks + 1) && (nextBricks <= currBricks + 1);
                if (valid) {
                    fieldState.karelPos.x = newPos.x;
                    fieldState.karelPos.y = newPos.y;
                }
            }

            frame.current++;
            return valid ? "OK" : "WARN";

        case "TURNLEFT":
            fieldState.karelPos.dir--;
            if (fieldState.karelPos.dir < 0) {
                fieldState.karelPos.dir = 3;
            }
            frame.current++;
            return "OK";

        case "PUT_BRICK":
            var newPos = getNextPos(fieldState.karelPos);
            var valid = positionInField(fieldState.field, newPos);
            if (valid) {
                if (fieldState.field[newPos.y][newPos.x].numBricks < maxBricks) {
                    fieldState.field[newPos.y][newPos.x].numBricks++;
                }
                else {
                    valid = false;
                }
            }
            frame.current++;
            return valid ? "OK" : "WARN";

        case "TAKE_BRICK":
            var newPos = getNextPos(fieldState.karelPos);
            var valid = positionInField(fieldState.field, newPos);
            if (valid) {
                if (fieldState.field[newPos.y][newPos.x].numBricks > 0) {
                    fieldState.field[newPos.y][newPos.x].numBricks--;
                }
                else {
                    valid = false;
                }
            }
            frame.current++;
            return valid ? "OK" : "WARN";

        case "PUT_MARK":
            var valid = false;
            if (fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numMarks < maxMarks) {
                fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numMarks++;
                valid = true;
            }
            frame.current++;
            return valid ? "OK" : "WARN";

        case "TAKE_MARK":
            var valid = false;
            if (fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numMarks > 0) {
                fieldState.field[fieldState.karelPos.y][fieldState.karelPos.x].numMarks--;
            }
            frame.current++;
            return valid ? "OK" : "WARN";

        case "REPEAT":
            if (stmt.count > 0) {
                evalStack.push({ current : 0, statements : stmt.body, continuation : "REPEAT", count : stmt.count - 1});
            }
            frame.current++;
            return "OK";

        case "WHILE":
            if (evalCondition(stmt.condition, fieldState)) {
                evalStack.push({ current : 0, statements : stmt.body, continuation : "WHILE", condition : stmt.condition});
            }
            frame.current++;
            return "OK";

        case "CALL":
            var body = environment[stmt.name];
            evalStack.push({ current : 0, statements : body });
            frame.current++;
            return "OK";

        case "IF":
            if (evalCondition(stmt.condition, fieldState)) {
                evalStack.push({ current : 0, statements : stmt.thenBody});
            }
            else if (stmt.elseBody) {
                evalStack.push({ current : 0, statements : stmt.elseBody});
            }
            frame.current++;
            return "OK";

        default:
            throw "Evaluation: Unknown statement";
    }
}

function parseProgram(p) {
    var lines = p.split("\n");
    var env = {};
    var body = [];
    var defs = {};
    var parseState = { lines : lines, lineNo : 0 };

    while (true) {
        var lineNo = parseState.lineNo;
        if (lineNo >= parseState.lines.length) {
            return { body : body, defs : defs };
        }
        var line = parseState.lines[parseState.lineNo].toUpperCase();

        var m = parseRes.defineRe.exec(line);
        if (m) {
            parseState.lineNo++;
            var name = m[1];
            var defBody = parseBlock(parseState, parseRes.endDefineRe);
            if (defs[name]) {
                throw "Function redef '" + name + "'.";
            }
            else {
                defs[name.toUpperCase()] = defBody;
            }
        }
        else {
            var stmt = parseStatement(parseState);

            if (stmt.kind !== "EMPTY") {
                body.push(stmt);
            }
        }
    }
}

var conditionsFromSyntax = {
    "CIHLA"   : "BRICK",
    "ZNACKA"  : "MARK",
    "VOLNO"   : "FREE",
    "SEVER"   : "NORTH" 
};

var simpleStatementsFromSyntax = {
    "KROK"          : "STEP",
    "VLEVOVBOK"     : "TURNLEFT",
    "POLOZ_ZNACKU"  : "PUT_MARK",
    "ZVEDNI_ZNACKU" : "TAKE_MARK",
    "POLOZ_CIHLU"   : "PUT_BRICK",
    "ZVEDNI_CIHLU"  : "TAKE_BRICK"
};

var complexStatementsToSyntax = {
    Repeat    : "OPAKUJ",
    While     : "DOKUD",
    If        : "KDYZ",
    EndRepeat : "KONEC_OPAKUJ",
    EndIf     : "KONEC_KDYZ",
    EndWhile  : "KONEC_DOKUD",
    Do        : "PROVED",
    Define    : "DEFINUJ",
    EndDefine : "KONEC_DEFINUJ",
    Not       : "NENI"
};

function generateRegExps() {
    var condReStr = "(|(" + complexStatementsToSyntax.Not + ")\\s+)(";
    var isFirst = true;
    for (var c in conditionsFromSyntax) {
        if (isFirst) {
            isFirst = false;
        }
        else {
            condReStr += "|";
        }
        condReStr += c;
    }
    condReStr += ")";

    var simpleStmtStr = "^\\s*(";
    isFirst = true;
    for (var s in simpleStatementsFromSyntax) {
        if (isFirst) {
            isFirst = false;
        }
        else {
            simpleStmtStr += "|";
        }
        simpleStmtStr += s;
    }
    simpleStmtStr += ")\\s*$";
    console.log(simpleStmtStr);

    var repeatStr = "^\\s*" + complexStatementsToSyntax.Repeat + "\\s+(\\d+)\\s*$";
    var whileStr = "^\\s*" + complexStatementsToSyntax.While + "\\s+" + condReStr + "\\s*$";
    var ifStr = "^\\s*" + complexStatementsToSyntax.If + "\\s+" + condReStr + "\\s*$";
    var endIfStr = "^\\s*" + complexStatementsToSyntax.EndIf + "\\s*$";
    var endRepeatStr = "^\\s*" + complexStatementsToSyntax.EndRepeat + "\\s*$";
    var endWhileStr = "^\\s*" + complexStatementsToSyntax.EndWhile + "\\s*$";
    var callStr = "^\\s*" + complexStatementsToSyntax.Do + "\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*$";
    var defineStr = "^\\s*" + complexStatementsToSyntax.Define + "\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*$";
    var endDefineStr = "^\\s*" + complexStatementsToSyntax.EndDefine + "\\s*$";
    var emptyStr = "^\\s*$";

    var res = {
        simpleStmtRe : new RegExp(simpleStmtStr, "i"),
        repeatRe : new RegExp(repeatStr, "i"),
        whileRe : new RegExp(whileStr, "i"),
        ifRe : new RegExp(ifStr, "i"),
        endIfRe : new RegExp(endIfStr, "i"),
        endRepeatRe : new RegExp(endRepeatStr, "i"),
        endWhileRe : new RegExp(endWhileStr, "i"),
        callRe : new RegExp(callStr, "i"),
        defineRe : new RegExp(defineStr, "i"),
        endDefineRe : new RegExp(endDefineStr, "i"),
        emptyRe : new RegExp(emptyStr, "i")
    };

    for (var r in res) {
        console.log(r);
        res[r].compile(res[r]);
    }

    return res;
}

var parseRes = generateRegExps();

function parseStatement(parseState) {
    var lineNo = parseState.lineNo;
    if (lineNo >= parseState.lines.length) {
        throw { kind : "ERROR", message : "Unexpected end of file", lineNo : lineNo };
    }
    var line = parseState.lines[parseState.lineNo].toUpperCase();
    var m = parseRes.simpleStmtRe.exec(line);
    if (m) {
        parseState.lineNo++;
        return { kind : simpleStatementsFromSyntax[m[1]], lineNo : lineNo };
    }

    m = parseRes.repeatRe.exec(line);
    if (m) {
        parseState.lineNo++;
        var n = m[1];
        var body = parseBlock(parseState, parseRes.endRepeatRe);
        return { kind : "REPEAT", body : body, count : n, lineNo : lineNo };
    }

    m = parseRes.whileRe.exec(line);
    if (m) {
        parseState.lineNo++;
        var condition = { kind : conditionsFromSyntax[m[3]], negated : m[2] === complexStatementsToSyntax.Not };
        var body = parseBlock(parseState, parseRes.endWhileRe);
        return { kind : "WHILE", body : body, condition : condition, lineNo : lineNo };
    }

    m = parseRes.ifRe.exec(line);
    if (m) {
        parseState.lineNo++;
        var condition = { kind : conditionsFromSyntax[m[3]], negated : m[2] === complexStatementsToSyntax.Not };
        var body = parseBlock(parseState, parseRes.endIfRe);
        return { kind : "IF", thenBody : body, condition : condition, lineNo : lineNo };
    }

    m = parseRes.callRe.exec(line);
    if (m) {
        parseState.lineNo++;
        return { kind : "CALL", name : m[1], lineNo : lineNo };
    }

    if (parseRes.emptyRe.test(line)) {
        parseState.lineNo++;
        return { kind : "EMPTY", lineNo : lineNo };
    }

    throw { kind : "ERROR", line : line, message : "Unknown statement", lineNo : lineNo };
}

function parseBlock(parseState, endRe) {
    var lineNo = parseState.lineNo;
    if (lineNo >= parseState.lines.length) {
        throw { kind : "ERROR", line : line, message : "Unexpected end of file in a block", lineNo : lineNo };
    }

    var line = parseState.lines[parseState.lineNo].toUpperCase();
    var statements = [];
    while (!endRe.test(line)) {
        var stmt = parseStatement(parseState);
        if (stmt.kind === "EOF") {
            throw { kind : "ERROR", line : line, message : "Unexpected end of file in a block", lineNo : lineNo };
        }
        if (stmt.kind !== "EMPTY") {
            statements.push(stmt);
        }
        line = parseState.lines[parseState.lineNo].toUpperCase();
    }
    parseState.lineNo++;
    return statements;
}
